Skip to content

Conversation

@Rich-Harris
Copy link
Member

Over on sveltejs/esrap#68 we're working on making esrap pluggable, so that it can be used to print any AST composed of { type: string, ... } nodes rather than just estree and its TypeScript extensions. That includes Svelte ASTs.

The main motivation for exposing this is so that we can make it easier to write preprocessors. Historically, Svelte exposed a preprocess API, but it's all strings and duct tape, and it's difficult to integrate preprocessors cleanly with bundlers as we've seen with enhanced-img.

When the preprocessor API was introduced, things looked very different. Preprocessing was necessary to support things like TypeScript and Sass. Today, TypeScript is supported natively, and CSS is sufficiently capable that Sass is little more than a historical curiosity.

In the long term, we'd therefore like to move away from the preprocessor API in favour of providing more robust lower-level utilities. For example enhanced-img, which can only be used in a Vite context, really should just be a Vite plugin:

import { parse, print } from 'svelte/compiler';
import { walk } from 'zimmerframe';

function transform(code) {
  const ast = parse(code);

  const transformed = walk(ast, null, {
    RegularElement(node, context) {
      if (node.name !== 'enhanced:img') return;
      // ...
    }
  });

  return print(ast);
}

There are other potential uses, such as migrations or sv add or having a 'format' button in the playground.

As a side-effect, quality of compiler output will be slightly better in certain cases, such as when encountering comments inside nodes. (Today, we attach leadingComments and trailingComments to each node, but this is a brittle and not-very-widely-used convention. The new esrap API expects an array of comments to be passed instead.)

This functionality already exists in svelte-ast-print (thank you @xeho91!), but having it in core means we can re-use the esrap version that's already installed alongside svelte, and will help ensure it stays current with new features.

Before submitting the PR, please make sure you do the following

  • It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs
  • Prefix your PR title with feat:, fix:, chore:, or docs:.
  • This message body should clearly illustrate what problems it solves.
  • Ideally, include a test that fails without this PR but passes with it.
  • If this PR changes code within packages/svelte/src, add a changeset (npx changeset).

Tests and linting

  • Run the tests with pnpm test and lint the project with pnpm lint

@changeset-bot
Copy link

changeset-bot bot commented Jun 17, 2025

🦋 Changeset detected

Latest commit: b0b315e

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
svelte Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@svelte-docs-bot
Copy link

@xeho91
Copy link
Contributor

xeho91 commented Jun 17, 2025

Nice!

I'll be happy to sunset svelte-ast-print in favour of built-in into the core print() function. 👍
I'm glad the need for it finally reached this point. I'm swamped right now with my real-life stuff, but just in case, both @manuel3108 and @paoloricciuti have access to the svelte-ast-print repository in case I don't manage to announce it on time.

manuel3108
manuel3108 previously approved these changes Jun 21, 2025
Copy link
Member

@manuel3108 manuel3108 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This already works super great.

I updated my pr for sv sveltejs/cli#568 to not only use parse but also print, and i basically didn't need to change anything in comparison to svelte-ast-print. I'm testing this with the paraglide addon, that is now printed nearly correctly, apart from the quoting issue mentioned below.

Apart from that we still need to figure out formatting, as the paraglide addon is currently printed in one line. This happens because I manually mutate the ast during the runtime and then print the ast.

I know all of this is in very early stages, but wanted to already leave some feedback

manuel3108

This comment was marked as outdated.

@manuel3108 manuel3108 dismissed their stale review June 21, 2025 10:13

unwanted

@manuel3108
Copy link
Member

manuel3108 commented Nov 24, 2025

I think this is ready now.

Here is a playground.
This will enable sv to use the svelte compiler directly, instead of doing a bunch of magic to parse and serialize svelte files: sveltejs/cli#751
The first example provided in the playground is from the demo integration from paraglide which i did also prepare in sveltejs/cli#751

Once this gets merged, you could do stuff like this:

import { parse, print } from 'svelte/compiler'

const ast = parse('<Whatever>svelte<span>code</span></Whatever>', { modern: true })
const output = print(ast).code
console.log(output)

The docs are pretty bare-bones, as with the other topics on the svelte/compiler page. But I do think they explain the most important points.

There is one major open point though, which I'm happy to discuss on Friday. It's related to adding new comments to the js-ast for the script-tag. This is currently a flaw in esrap, which we should solve before merging this: sveltejs/esrap#79. Whatever we do in esrap, should probably also be exposed here.

Disclaimer:

  • formatting is way from perfect in certain cases, but that's not a problem:
    • we can easily adjust that later on in follow-up prs
    • you should run prettier (or any code formatter) over your files as soon as they got modified. The intent here is not to produce a 1:1 output,
  • I'm not sure if we currently support all nodes / types that we have. But support should be pretty good, and it's easy to add stuff later on.

@manuel3108 manuel3108 marked this pull request as ready for review November 24, 2025 19:24
@Rich-Harris
Copy link
Member Author

Awesome! I opened sveltejs/esrap#90 as an alternative to sveltejs/esrap#79; should be easier enough to bring either over.

Spotted one thing we have to fix before we can merge this — we mustn't generate self-closing tags for HTML elements. Certainly not the non-void ones. <div></div> and <div /> are not the same as far as HTML is concerned. For void elements it's less clear-cut — <input> and <input /> are equivalent, but only because the closing solidus is ignored. Technically, <input> is correct.

Couple of other nits I'd love to see us fix at some point (though as you say, it needn't block this PR) — multiline blocks ought to have a margin between them:

if (true) {
  console.log('multiline blocks in JS get a margin between them');
}

if (true) {
  console.log('like that');
}
<div>
  <p>but multiline blocks in Svelte...</p>
</div>
<div>
  <p>...don't. It looks very cramped</p>
</div>

And this...

<div data-one="1" data-two="two" data-three="3"></div>
<div data-one="1" data-two="two" data-three="3">text</div>

...gets turned into this:

<div
	data-one="1"
	data-two="two"
	data-three="3" />
<div
	data-one="1"
	data-two="two"
	data-three="3">
	text
</div>

Aside from the self-closing thing, I think this is arguably more conventional — the > of the opening tag at the same indentation level as the <:

<div
	data-one="1"
	data-two="two"
	data-three="3"
></div>

<div
	data-one="1"
	data-two="two"
	data-three="3"
>
	text
</div>

@Rich-Harris
Copy link
Member Author

If we do get the formatting looking nice enough, we could add a 'format' button to the playground. that'd be cool


const multiline_attributes = attributes(node.attributes, child_context);

const is_self_closing = is_void(node.name) || node.fragment.nodes.length === 0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as stated in #16188 (comment)

Suggested change
const is_self_closing = is_void(node.name) || node.fragment.nodes.length === 0;
const is_self_closing = is_void(node.name) || ((/^[A-Z]/.test(node.name) || node.name.includes('.')) && node.fragment.nodes.length === 0);

only void elements and components with no children should be considered self-closing

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants